/*
 * DocumentController.java
 *
 * Created on November 3, 2006, 8:15 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package edu.umn.cs5115.scheduler.framework;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Shell;

/**
 * This class manages open documents.  It also handles opening and creating new
 * documents, and keeps the list of recent documents.  There is only one
 * DocumentController per application, which you can retrieve using 
 * getSharedDocumentController()
 * @see DocumentController#getSharedDocumentController()
 * @author grant
 */
public class DocumentController extends KeyValueCodingBase
{
    /** Subscribe to notifications on this key to receive notifications about
     * changes in the currently focused control. */
    public final static String FOCUS_CONTROL_KEY = "focusControl";
    
    /** The document that is active, and needs undo grouping performed before 
     * and after the event loop. */
    private Document activeDocument;
    /** The single connnection to the system used by this application. */
    private Display display;
    /** The list of open documents.  Used when trying to close all documents before quitting. */
    private IteratorSafeSet<Document> openDocuments = new IteratorSafeSet();
    /** Set to false when the application should termiante. */
    private boolean stillAlive = true;
    /** Used to create instances of Documents. */
    private DocumentFactory documentFactory;
    /** The top-level application shell.  It's never displayed, but used when
     * we need to display an open dialog. */
    private Shell applicationShell;
    /** Object for creating the main menu bar. */
    private MainMenu mainMenu;
    /** A count of how many new documents we've created so we can number them. (Untitled 1, Untitled 2, Untitled 3...) */
    private int newDocumentCount;
    /** Currently focused Control. */
    private Control focusControl;
    
    /** This is the DocumentController instance that will be used by this application.
     * It can be retrieved using getSharedDocumentController
     */
    private static DocumentController sharedController = new DocumentController();

    /** Error message used when converting a file to a URL fails. */
    private final static String FILE_URL_UNSUPPORTED_ERROR_MESSAGE = "Your system does not support file URLs.  The application cannot function without this support.";
    
    /** Creates a new instance of DocumentController */
    protected DocumentController()
    {
    }
    
    /** Get the shared document controller used by this application.
     * @return The document controller that is used by this application.
     */
    public static DocumentController getSharedDocumentController()
    {
        return sharedController;
    }
    
    /** Show a file chooser and open a document. */
    public void open()
    {
        if (documentFactory == null)
            throw new IllegalStateException("You MUST set the DocumentFactory to " +
                    "something non-null before creating new documents or opening " +
                    "existing documents.");
        FileDialog openDialog = new FileDialog(getAnyShell(), SWT.OPEN);
        String openedFilePath = openDialog.open();
        if (openedFilePath != null)
        {
            try
            {
                URL openedURL = new File(openedFilePath).toURI().toURL();
                open(openedURL);
            } catch (MalformedURLException ex) {
                // This means that file:// URL's aren't supported.  It's very very
                // unlikely that this will actually occur.
                ex.printStackTrace();
                displayApplicationError(FILE_URL_UNSUPPORTED_ERROR_MESSAGE);
            }
        }
    }

    /**
     * Open a specific document.  This creates a new document using the document
     * factory and asks it to read from the given URL.  The new document is 
     * added to the list of open documents.
     * @param fileURL The URL of the file to open.
     */
    public void open(URL fileURL)
    {
        Document newDocument = getPristineDocument();
        boolean wasPristine = (newDocument != null);
        if (!wasPristine)
        {
            newDocument = documentFactory.createDocument();
            openDocuments.add(newDocument);
        }
        if (newDocument.readFromURL(fileURL))
        {
            if (!wasPristine)
                newDocument.createWindowControllers();
            else
                newDocument.setPristine(false);
        } else {
            if (!wasPristine)
                openDocuments.remove(newDocument);
            displayApplicationError("The document \""+fileURL+"\" could not be read.");
        }
    }
    
    /**
     * Finds and returns a pristine document that is open, if one exists.
     * @return The pristine document.  If no document is pristine, this method
     * returns null.
     */
    public Document getPristineDocument()
    {
        // if there is a pristine document, there is only one document open and
        // it is the first one that is pristine.
        if (openDocuments.size() == 1)
        {
            Document firstDocument = (Document)openDocuments.iterator().next();
            if (firstDocument.isPristine())
                return firstDocument;
        }
        return null;
    }
    
    /** 
     * Create a new document.
     * @throws IllegalStateException if the documentFactory has not been set.
     * @see #setDocumentFactory(DocumentFactory)
     */
    public void newDocument()
    {
        if (documentFactory == null)
            throw new IllegalStateException("You MUST set the DocumentFactory to " +
                    "something non-null before creating a new document or attempting " +
                    "to run the application.");
        Document newDocument = documentFactory.createDocument();
        if (activeDocument == null)
            activeDocument = newDocument;
        newDocument.createWindowControllers();
        openDocuments.add(newDocument);
        newDocumentCount++;
        newDocument.setDisplayName("Untitled " + newDocumentCount);
        
        // clear all pristine markers
        Iterator iterator = openDocuments.iterator();
        while(iterator.hasNext())
        {
            Document thisDocument = (Document) iterator.next();
            thisDocument.setPristine(false);
        }
    }
    
    /** Gracefully terminate the application.  All open documents will need to
     * be closed before the application will actually end. */
    public void quit()
    {
        openDocuments.startIterating();
        for(Document thisDocument : openDocuments)
            thisDocument.close();
        openDocuments.finishIterating();
        if (openDocuments.size() == 0)
        {
            if (applicationShell != null)
                applicationShell.dispose();
            stillAlive = false;
        }
    }

    /**
     * Clears the open recent menu used in all menu bars.
     */
    public void clearOpenRecentMenu()
    {
        
    }
    
    /** 
     * Sets the active document.  You must do this for automatic undo grouping 
     * to work properly.  Normally, WindowControllers take care of this for you.
     * @param document The document to set as the active document.
     */
    public void setActiveDocument(Document document)
    {
        activeDocument = document;
    }
    
    /**
     * Gets the active document.  This is the document with the frontmost window
     * in the application.
     * @return The document that is currently active.  It may be null if no
     * documents are open.
     */
    public Document getActiveDocument()
    {
        return activeDocument;
    }
    
    /** Gets the currently focused control.  This is identical to the value 
     * returned by getDisplay().getFocusControl().
     * @return The currently focused control.  May be null if there is no 
     * focused control, or the control that has focus
     */
    public Control getFocusControl()
    {
        return focusControl;
    }

    /**
     * Sets the currently focused control.  Sends notifications if this is a
     * change from the old focused control.
     * @param focusControl The newly focused control.
     */
    private void setFocusControl(Control focusControl)
    {
        if (this.focusControl != focusControl)
        {
            this.focusControl = focusControl;
            didChangeValueForKey(FOCUS_CONTROL_KEY);
        }
    }
    
    /**
     * Get the shared connection to the OS used by SWT for drawing.  It is 
     * important that you use this display rather than creating your own, since
     * SWT does not guarantee that applications will work properly if there is
     * an attempt to create multiple active displays.
     * @return The display in use by this application.
     */
    public Display getDisplay()
    {
        return display;
    }
    
    /**
     * Set the document factory that is used to create new instances of Document.
     * You should call this method to allow DocumentController to create 
     * instances of your custom Document subclass.
     * @param documentFactory an instance DocumentFactory that can create your
     * custom Document subclass.
     * @see DocumentController.DocumentFactory
     */
    public void setDocumentFactory(DocumentFactory documentFactory)
    {
        this.documentFactory = documentFactory;
    }
    
    /**
     * Get the application's main menu bar manager.  You should use this to 
     * create menu bars for your windows.
     * @return The MainMenu manager for menu bars.
     */
    public MainMenu getMainMenu()
    {
        if (mainMenu == null)
            mainMenu = new MainMenu();
        return mainMenu;
    }
    
    /**
     * Sets the instance of MainMenu to be used to create menu bars for the
     * application.  You should set this once, before calling run, as menu bars
     * that are created won't be updated after you set this.
     * @param menu The MainMenu to use to create all the application's menu bars.
     */
    public void setMainMenu(MainMenu menu)
    {
        mainMenu = menu;
    }
    
    /**
     * Create the connection to the OS's display and run the application's event
     * loop.
     */
    public void run()
    {
        display = new Display();
        
        // create the application shell, used to display application modal 
        // dialogs. (like the open dialog)
        if (Application.getOS() == Application.OS.MAC_OS_X)
        {
            applicationShell = new Shell(display, SWT.NO_TRIM);
        
            applicationShell.setSize(0, 0);
            getMainMenu().createMenuBar(applicationShell, null);
            applicationShell.open();
        }
        
        // create a blank new document.
        newDocument();
        if (openDocuments.size() > 0)
            ((Document)openDocuments.iterator().next()).setPristine(true);
        
        // application's main loop.
        while (stillAlive)
        {
            Document currentActiveDocument = getActiveDocument();
            if (currentActiveDocument != null)
                currentActiveDocument.getUndoManager().beginUndoGrouping();
            
            // process an event
            boolean moreToDo;
            try
            {
                moreToDo = display.readAndDispatch();
            } catch (Throwable e) {
                e.printStackTrace();
                displayApplicationError("An unhandled " + e.getClass().getName() + " was caught.  If you are trying to debug the application, a message has been printed to the console.");
                moreToDo = true;
            }
            // set the currently focused widget, thereby notifying listeners.
            if (!display.isDisposed())
                setFocusControl(display.getFocusControl());
            
            if (currentActiveDocument != null)
                currentActiveDocument.getUndoManager().endUndoGrouping();
            
            // sleep until the next event is ready (or, if another one is already
            // ready, just head back to the top of the loop and start over.
            if (!moreToDo)
                display.sleep();
            
            if (applicationShell != null)
                stillAlive &= !applicationShell.isDisposed();
        }
        display.dispose();
    }

    /**
     * Returns the application shell, that is used to produce the menu bar.  You
     * can use this shell if you need to produce a dialog but don't have a shell
     * for some reason.<p>
     * @return The application's main shell.
     * @deprecated Only works on Mac OS X.  Use getAnyShell() instead.
     * @see #getAnyShell()
     */
    public Shell getApplicationShell()
    {
        return applicationShell;
    }
    
    /**
     * Returns a shell.
     * On Mac OS X, the shell returned is the application shell.
     * On Windows and Linux, the shell returned is the active document's front-
     * most window.
     * @return A shell from the application that can be used to parent dialogs
     * and such.  May return null during application start up, or whenever there
     * are no windows open for any reason.
     */
    public Shell getAnyShell()
    {
        if (applicationShell != null)
            return applicationShell;
        Document activeDocument = getActiveDocument();
        if (activeDocument != null)
        {
            Shell theShell = activeDocument.getMainWindow();
            if (theShell == null)
                theShell = display.getActiveShell();
            return theShell;
        }
        return null;
    }
    
    /**
     * Display a quick application-level error.  Avoid calling this function
     * if at all possible, of course.
     * @param errorText The error message to display.
     */
    public void displayApplicationError(String errorText)
    {
        MessageBox error = new MessageBox(getAnyShell(), SWT.ICON_ERROR | SWT.OK);
        error.setMessage(errorText);
        error.open();
    }

    /**
     * Called by documents when they finish closing.  This removes them from the
     * list of open documents.
     * @param document The document that closed.
     */
    void documentDidClose(Document document)
    {
        openDocuments.remove(document);
        // For non-Mac OS X operating systems, close the program when the last 
        // open document closed
        if (openDocuments.actualSize() == 0 && Application.getOS() != Application.OS.MAC_OS_X)
            quit();
    }
    
    /**
     * A Class that can create instances of your Document subclass on demand.
     * @see DocumentController.DocumentFactory#createDocument()
     */
    public static interface DocumentFactory
    {
        /**
         * Create a new instance of some subclass of Document, and return it.
         * @return A new instance of your custom subclass of Document.
         */
        public Document createDocument();
    }
}
